/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.utils;

import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;

import java.util.List;

public class StringBuilderUtils {
    /**
     * Breaking separator inserted between text, intended to make TTS pause an
     * appropriate amount. Using a period breaks pronunciation of street
     * abbreviations, and using a new line doesn't work in eSpeak.
     */
    public static final String DEFAULT_BREAKING_SEPARATOR = ", ";

    /**
     * Non-breaking separator inserted between text. Used when text already ends
     * with some sort of breaking separator or non-alphanumeric character.
     */
    public static final String DEFAULT_SEPARATOR = " ";

    /**
     * The hex alphabet.
     */
    private static final char[] HEX_ALPHABET = "0123456789abcdef".toCharArray();

    /**
     * Generates the aggregate text from a list of {@link CharSequence}s,
     * separating as necessary.
     *
     * @param textList The list of text to process.
     * @return The separated aggregate text, or null if no text was appended.
     */
    public static CharSequence getAggregateText(List<CharSequence> textList) {
        final CharSequence aggregateText;
        if (textList == null || textList.isEmpty()) {
            aggregateText = null;
        } else {
            final SpannableStringBuilder builder = new SpannableStringBuilder();
            for (CharSequence text : textList) {
                appendWithSeparator(builder, text);
            }

            aggregateText = builder;
        }

        return aggregateText;
    }

    /**
     * Appends CharSequence representations of the specified arguments to a
     * {@link SpannableStringBuilder}, creating one if the supplied builder is
     * {@code null}. A separator will be inserted between each of the arguments.
     *
     * @param builder An existing {@link SpannableStringBuilder}, or {@code null} to create one.
     * @param args    The objects to append to the builder.
     * @return A builder with the specified objects appended.
     */
    public static SpannableStringBuilder appendWithSeparator(
            SpannableStringBuilder builder, CharSequence... args) {
        if (builder == null) {
            builder = new SpannableStringBuilder();
        }

        for (CharSequence arg : args) {
            if (arg == null) {
                continue;
            }

            if (arg.toString().length() == 0) {
                continue;
            }

            if (builder.length() > 0) {
                if (needsBreakingSeparator(builder)) {
                    builder.append(DEFAULT_BREAKING_SEPARATOR);
                } else {
                    builder.append(DEFAULT_SEPARATOR);
                }
            }

            builder.append(arg);
        }

        return builder;
    }

    /**
     * Appends CharSequence representations of the specified arguments to a
     * {@link SpannableStringBuilder}, creating one if the supplied builder is
     * {@code null}. A separator will be inserted before the first non-{@code null} argument,
     * but additional separators will not be inserted between the following elements.
     *
     * @param builder An existing {@link SpannableStringBuilder}, or {@code null} to create one.
     * @param args    The objects to append to the builder.
     * @return A builder with the specified objects appended.
     */
    public static SpannableStringBuilder append(
            SpannableStringBuilder builder, CharSequence... args) {
        if (builder == null) {
            builder = new SpannableStringBuilder();
        }

        boolean didAppend = false;
        for (CharSequence arg : args) {
            if (arg == null) {
                continue;
            }

            if (arg.toString().length() == 0) {
                continue;
            }

            if (builder.length() > 0) {
                if (!didAppend && needsBreakingSeparator(builder)) {
                    builder.append(DEFAULT_BREAKING_SEPARATOR);
                } else {
                    builder.append(DEFAULT_SEPARATOR);
                }
            }

            builder.append(arg);
            didAppend = true;
        }

        return builder;
    }

    /**
     * Create spannable from text that includes some CharSequence. If the CharSequence has any spans
     * they would be copied to result spannable
     * @param text - some text that potentially contains CharSequence template
     * @param innerTemplate - CharSequence that is supposed but not necessary to be Spanned.
     *                      If it is Spanned the spans are copied to result Spannable
     * @return Spannable object that contains incoming text and spans from innerTemplate
     */
    public static Spannable createSpannableFromTextWithTemplate(String text,
                                                                CharSequence innerTemplate) {
        SpannableString result = new SpannableString(text);
        if(innerTemplate instanceof Spanned) {
            int index = text.indexOf(innerTemplate.toString());
            if(index >= 0) {
                copySpans(result, (Spanned) innerTemplate, index);
            }
        }

        return result;
    }

    /**
     * Utility that copies spans from fromSpan to toSpan
     * @param toSpan - Spannable that is supposed to contain fromSpan.
     * @param fromSpan - Spannable that could contain spans that would be copied to toSpan
     * @param startIndex - Starting index of occurrence fromSpan in toSpan
     */
    private static void copySpans(Spannable toSpan, Spanned fromSpan, int startIndex) {
        if (startIndex < 0 || startIndex >= toSpan.length()) {
            LogUtils.log(StringBuilderUtils.class, Log.ERROR, "startIndex parameter (" +
                    startIndex + ") is out of toSpan length " + toSpan.length());
            return;
        }

        Object[] spans = fromSpan.getSpans(0, fromSpan.length(), Object.class);
        if (spans != null && spans.length > 0) {
            for (Object span : spans) {
                int spanStartIndex = fromSpan.getSpanStart(span);
                int spanEndIndex = fromSpan.getSpanEnd(span);
                if(spanStartIndex >= spanEndIndex) {
                    continue;
                }

                int spanFlags = fromSpan.getSpanFlags(span);
                toSpan.setSpan(span, startIndex + spanStartIndex, startIndex + spanEndIndex, spanFlags);
            }
        }
    }

    /**
     * Returns whether the text needs a breaking separator (e.g. a period
     * followed by a space) appended before more text is appended.
     * <p>
     * If text ends with a letter or digit (according to the current locale)
     * then this method will return {@code true}.
     */
    private static boolean needsBreakingSeparator(CharSequence text) {
        return !TextUtils.isEmpty(text)
                && Character.isLetterOrDigit(text.charAt(text.length() - 1));
    }

    /**
     * Convert a byte array to a hex-encoded string.
     *
     * @param bytes The byte array of data to convert
     * @return The hex encoding of {@code bytes}, or null if {@code bytes} was null
     */
    public static String bytesToHexString(byte[] bytes) {
        if (bytes == null) {
            return null;
        }

        final StringBuilder hex = new StringBuilder(bytes.length * 2);
        int nibble1, nibble2;
        for (byte b : bytes) {
            nibble1 = (b >>> 4) & 0xf;
            nibble2 = b & 0xf;
            hex.append(HEX_ALPHABET[nibble1]);
            hex.append(HEX_ALPHABET[nibble2]);
        }

        return hex.toString();
    }
}
